Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 1ed042344b8dea4a62c9247af5901122246b16e2


Parents : 494942d
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-05-09T21:19:12-05:00

fix(android): prevent RNodeInterface crash and use external storage

- Disable RNodeInterface/RNodeIPInterface/RNodeMultiInterface in Reticulum

config on Android before startup to prevent crashes from missing

serial/BLE support in Chaquopy

- Use Android getExternalFilesDir() as default storage on Android so

databases and config are user-accessible via file managers

- Add _get_android_external_files_dir() helper to android_push_bridge

Changes

3 files changed, 165 insertions(+), 2 deletions(-)


Diff

diff --git a/meshchatx/android_push_bridge.py b/meshchatx/android_push_bridge.py
index 0291f14d..d8259776 100644
--- a/meshchatx/android_push_bridge.py
+++ b/meshchatx/android_push_bridge.py
@@ -21,6 +21,24 @@ def _is_chaquopy_android() -> bool:
return True
+def _get_android_external_files_dir() -> str | None:
+ """Return the Android app-specific external files directory, or None.
+
+ This path is user-accessible via file managers (Android/data/<pkg>/files).
+ """
+ if not _is_chaquopy_android():
+ return None
+ try:
+ from com.chaquo.python import Python
+ context = Python.getPlatform().getApplication()
+ external = context.getExternalFilesDir(None)
+ if external is not None:
+ return str(external.getAbsolutePath())
+ except Exception:
+ pass
+ return None
+
+
def lxmf_delivery_notification_text(payload: dict[str, Any]) -> tuple[str, str] | None:
"""Return (title, body) for a system notification, or None to skip."""
if payload.get("type") != "lxmf.delivery":

diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py
index 7ff3a7e9..c4ac64ff 100644
--- a/meshchatx/meshchat.py
+++ b/meshchatx/meshchat.py
@@ -143,7 +143,10 @@ from meshchatx.src.backend.sticker_utils import (
validate_export_document,
)
from meshchatx.src.backend.telemetry_utils import Telemeter
-from meshchatx.android_push_bridge import _is_chaquopy_android
+from meshchatx.android_push_bridge import (
+ _get_android_external_files_dir,
+ _is_chaquopy_android,
+)
from meshchatx.src.backend.web_audio_bridge import WebAudioBridge
from meshchatx.src.env_utils import env_bool
from meshchatx.src.path_utils import (
@@ -695,6 +698,42 @@ class ReticulumMeshChat:
return True
return False
+ @staticmethod
+ def _disable_rnode_interfaces_on_android(config_path: str) -> bool:
+ """If running on Android, disable RNode* interfaces in Reticulum config.
+
+ Returns True if any interfaces were disabled.
+ """
+ if not _is_chaquopy_android():
+ return False
+ if not os.path.isfile(config_path):
+ return False
+ try:
+ from RNS.vendor.configobj import ConfigObj
+
+ cfg = ConfigObj(config_path)
+ except Exception:
+ return False
+
+ modified = False
+ interfaces = cfg.get("interfaces")
+ if not isinstance(interfaces, dict):
+ return False
+ for _iface_name, iface in interfaces.items():
+ if not isinstance(iface, dict):
+ continue
+ iface_type = iface.get("type", "")
+ if isinstance(iface_type, str) and iface_type.startswith("RNode"):
+ if str(iface.get("interface_enabled", "")).lower() in ("true", "yes", "1", "on"):
+ iface["interface_enabled"] = "false"
+ modified = True
+ if modified:
+ try:
+ cfg.write()
+ except Exception:
+ pass
+ return modified
+
def _ensure_reticulum_config(self, materialize: bool = True):
"""Normalize ``reticulum_config_dir`` and optionally ensure a ``config`` file exists.
@@ -725,6 +764,13 @@ class ReticulumMeshChat:
if not os.path.isdir(config_dir):
os.makedirs(config_dir, exist_ok=True)
self._write_rns_reticulum_default_config_file(config_path)
+ # Android: RNodeInterface crashes because serial port access isn't available
+ if _is_chaquopy_android():
+ disabled = self._disable_rnode_interfaces_on_android(config_path)
+ if disabled:
+ logging.getLogger(__name__).warning(
+ "RNodeInterface is not supported on Android; disabled in config.",
+ )
def setup_identity(self, identity: RNS.Identity):
identity_hash = identity.hash.hex()
@@ -17592,7 +17638,14 @@ def main():
if args.no_crash_recovery:
recovery.disable()
- planned_storage_dir = args.storage_dir or os.path.join("storage")
+ planned_storage_dir = args.storage_dir
+ if not planned_storage_dir:
+ # On Android, prefer user-accessible external storage
+ android_external = _get_android_external_files_dir()
+ if android_external:
+ planned_storage_dir = android_external
+ else:
+ planned_storage_dir = os.path.join("storage")
effective_storage_dir, migration_context = resolve_startup_storage(
planned_storage_dir,
)

diff --git a/tests/backend/test_android_rnode.py b/tests/backend/test_android_rnode.py
new file mode 100644
index 00000000..21be138b
--- /dev/null
+++ b/tests/backend/test_android_rnode.py
@@ -0,0 +1,92 @@
+# SPDX-License-Identifier: 0BSD
+
+import os
+
+import pytest
+
+from meshchatx.meshchat import ReticulumMeshChat
+
+
+def test_disable_rnode_interfaces_on_android(tmp_path):
+ config_path = tmp_path / "config"
+ config_path.write_text(
+ """[reticulum]
+enable_transport = False
+
+[interfaces]
+ [[RNode Serial]]
+ type = RNodeInterface
+ interface_enabled = True
+ port = /dev/ttyUSB0
+ frequency = 867200000
+ bandwidth = 125000
+ txpower = 7
+ spreadingfactor = 8
+ codingrate = 5
+
+ [[TCP Client]]
+ type = TCPClientInterface
+ interface_enabled = True
+ target_host = localhost
+ target_port = 4242
+""",
+ encoding="utf-8",
+ )
+
+ with pytest.MonkeyPatch.context() as mp:
+ mp.setattr(
+ "meshchatx.meshchat._is_chaquopy_android",
+ lambda: True,
+ )
+ modified = ReticulumMeshChat._disable_rnode_interfaces_on_android(
+ str(config_path),
+ )
+
+ assert modified is True
+ content = config_path.read_text(encoding="utf-8")
+ assert "interface_enabled = false" in content
+ assert "type = RNodeInterface" in content
+ assert "type = TCPClientInterface" in content
+
+
+def test_disable_rnode_interfaces_skips_when_not_android(tmp_path):
+ config_path = tmp_path / "config"
+ config_path.write_text(
+ """[interfaces]
+ [[RNode Serial]]
+ type = RNodeInterface
+ interface_enabled = True
+""",
+ encoding="utf-8",
+ )
+
+ with pytest.MonkeyPatch.context() as mp:
+ mp.setattr(
+ "meshchatx.meshchat._is_chaquopy_android",
+ lambda: False,
+ )
+ modified = ReticulumMeshChat._disable_rnode_interfaces_on_android(
+ str(config_path),
+ )
+
+ assert modified is False
+ content = config_path.read_text(encoding="utf-8")
+ assert "interface_enabled = True" in content
+
+
+def test_disable_rnode_interfaces_handles_missing_config():
+ with pytest.MonkeyPatch.context() as mp:
+ mp.setattr(
+ "meshchatx.meshchat._is_chaquopy_android",
+ lambda: True,
+ )
+ modified = ReticulumMeshChat._disable_rnode_interfaces_on_android(
+ "/nonexistent/config",
+ )
+ assert modified is False
+
+
+def test_get_android_external_files_dir_returns_none_on_desktop():
+ from meshchatx.android_push_bridge import _get_android_external_files_dir
+
+ assert _get_android_external_files_dir() is None


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────